Skip to content

docs(adr-0104): step 3's abort is a pre-check, not a property of the retype - #16972

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-16183-adr-0104-step3-abort-precheck
Sep 9, 2026
Merged

docs(adr-0104): step 3's abort is a pre-check, not a property of the retype#16972
os-zhuang merged 1 commit into
mainfrom
claude/issue-16183-adr-0104-step3-abort-precheck

Conversation

@os-musk

@os-musk os-musk commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Fixes #16183

Clause-②: no

Part of #15989 — that card is not addressed here and remains open.

⛔ GOVERNED SURFACE — this PR is a TERMINAL DRAFT

The diff's only path is docs/adr/0104-field-runtime-value-shape-contract.md, a governed surface under Prime Directive #14. ⇒ this PR is docs-only, stays a draft, and is merged by a human. It is never marked ready for review, never enqueued, and auto-merge is never armed on it. No code rides along; content/docs/releases/** is untouched.

What the diff does

ADR-0104's 2026-09-05 addendum promises that step 3 of os migrate files-to-references --apply moves a media column while "aborting on the first cell that is not a JSON string", and the paragraph below it prescribes a Postgres USING clause that "unquotes the JSON string". Those two sentences contradict each other.

The requirement is correct and is unchanged — the phrase survives byte-for-byte (verified whitespace-normalised: exactly one occurrence, before and after). What was optimistic is the statement list under it. Three edits:

  1. The step-3 pipeline block now reads as 3a (a pre-check over every cell of every media column the step is about to move, run before any DDL or rewrite, carrying the requirement phrase verbatim) and 3b (the move, run only if 3a found none).
  2. The sketch paragraph gains an amendment marker in this ADR's established in-addendum form, plus what the two rehearsals returned and the measured discriminator per dialect.
  3. Confidence gaps 1 and 2 record the rehearsal: gap 1 gains its single Postgres exception, gap 2 is partly closed by a result that was negative.

The rehearsals — re-run here, not cited from triage

Triage stated it did not re-run them, so both were re-run in this container before being written into a governed document as normative statements.

Tree: branch claude/issue-16183-adr-0104-step3-abort-precheck, base origin/main at bc1c1ce494, final commit c155e20a78.

Postgres — server version PostgreSQL 16.13 (Ubuntu 16.13-0ubuntu0.24.04.1) on x86_64-pc-linux-gnu. A json column holding three JSON-quoted ids plus one un-backfilled inline object. Run under psql -v ON_ERROR_STOP=1, whose exit 0 is itself the proof no statement aborted:

=== PRE-CHECK: the discriminator the card reports ===
 non_string_cells
------------------
                1

=== per-cell json_typeof ===
 id |      note      | typeof
----+----------------+--------
  1 | backfilled     | string
  2 | backfilled     | string
  3 | backfilled     | string
  4 | NOT backfilled | object

=== THE RETYPE the ADR prescribes — does it abort? ===
ALTER TABLE            (ACCEPTED — no abort, no error)

=== what the un-backfilled row now holds ===
  4 | NOT backfilled | {"url":"https://x/y.png"}     (flattened into varchar(2048))

SQLite — sqlite_version() = 3.51.2 (node:sqlite, Node v22.22.2), same four-cell shape plus an already-bare cell. The json_type = 'text' gate converted the two quoted cells (changes = 2), left the already-bare cell untouched, was idempotent on re-run (before === after true) — and left the inline-object cell silently unconverted rather than refusing it.

⇒ Neither rehearsed statement performs the abort. The two failure modes differ (Postgres destroys that cell's structure; SQLite leaves it in the old encoding) and neither is the stated abort. That is the whole basis for 3a.

⚠️ Two things the new text keeps NOT MEASURED

Both are written into the ADR as still open, and neither is "completed":

  • MySQL is entirely unmeasured. Gap 2's ordering question there (the retype must precede the unquote, or the two must be one statement) is untouched by this PR, and MySQL's abort discriminator is stated as unknown.
  • Whether some other Postgres realisation could abort on its own is unmeasured. #>> '{}' is the idiom the ADR's words describe and it does not abort; the new text calls anything else an open question, not a finding.

The ADR's decision is not re-opened: D1/D3, the window and the end-state column are untouched.

Changeset route — measured, not assumed

Route chosen: skip-changeset. The measurement, not the assumption that "a docs path publishes nothing":

  • git diff --name-only against the merge base = exactly 1 path, docs/adr/0104-field-runtime-value-shape-contract.md.
  • No workspace glob in pnpm-workspace.yaml covers docs/** (the globs are packages/*, packages/apps/*, packages/drivers/*, packages/plugins/*, packages/qa/*, packages/triggers/*, packages/services/*, packages/adapters/*, packages/connectors/*, apps/*, examples/*) ⇒ the path belongs to no workspace package.
  • The nearest ancestor package.json is the repo root, @objectstack/spec-monorepo, private: true ⇒ not a released package. There is no docs/package.json.
  • Enumerated over git ls-files: 70 non-private named package.json files. None is an ancestor directory of docs/adr, and none has a files[] entry containing docs or .., so no published artifact can reach the path.

⇒ this diff publishes nothing from any released package, which is exactly the condition AGENTS.md's changeset floor sets for the label (that sentence is at AGENTS.md:1036 on origin/main and on this branch; the same sentence sits at :1019 in the shared checkout).

Gates — COVERAGE and VERDICTS, stated separately

Derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, re-derived after a fresh git fetch origin main (byte-identical command list both times).

COVERAGE (what was accounted for, independent of pass/fail):

Run reconciliation — 17 derived, 17 run, 0 NOT-MEASURED, 0 UNRUN.
✓ dispatch-gates --ran: 17 derived famil(ies) accounted for — 17 run, 0 NOT-MEASURED.

Not covered locally, by design: the always-runs tail, the unreachable listing, the two families taking a value from the workflow, and the 13 families that only apply once a changeset path exists — this PR deliberately has none. CI owns those.

VERDICTS (exit codes captured by redirect-then-$?, never across a pipe — cmd > log 2>&1; EXIT=$?):

EXIT=0   node scripts/check-adr-links.mjs
EXIT=0   node scripts/check-adr-links.mjs --self-test
EXIT=0   node scripts/check-adr-symbol-anchors.mjs
EXIT=0   node scripts/check-adr-symbol-anchors.mjs --self-test
EXIT=0   node scripts/check-ci-filter-parity.mjs
EXIT=0   node scripts/check-closing-keyword-parity.mjs
EXIT=0   node scripts/check-closing-keyword-parity.mjs --self-test
EXIT=0   node scripts/check-comment-mask-corpus.mjs
EXIT=0   pnpm check:adr-anchors
EXIT=0   pnpm check:cross-package-test-inputs
EXIT=0   pnpm check:doc-authoring
EXIT=0   pnpm check:driver-memory-census
EXIT=0   pnpm check:nul-bytes
EXIT=0   pnpm check:pm-governed-merges
EXIT=0   pnpm check:refd-timer-probe
EXIT=0   pnpm check:watch-hint-literal
EXIT=0   pnpm --filter @objectstack/lint run check:doc-formula-expressions

That last one first returned EXIT=3 PREREQUISITE NOT MET (@objectstack/formula and @objectstack/lint not built). Exit 3 is NOT MEASURED, not a pass, so the prerequisite was satisfied — pnpm exec turbo run build --filter=@objectstack/formula --filter=@objectstack/lint --concurrency=2 under the shared verify lock, VERDICT command-exit 0 — and the gate re-run to a real EXIT=0.

Repo-wide eslint (pnpm lint = eslint . --no-inline-config) is CI's run, and the narrowing here is a measurement rather than a skip:

  1. Population read from eslint's own configuration, not guessed — eslint --format json on the changed file reports "File ignored because no matching configuration was supplied."; eslint.config.mjs declares its file kinds as {ts,tsx,mts,cts,js,jsx,mjs,cjs} with zero markdown.
  2. File count read from --format json: 1 file examined, errorCount: 0.
  3. Invariance for untouched files: the diff changes no eslint config, no tsconfig and no package.json, and its one path is a .md eslint never reads ⇒ no untouched file's verdict can move.

Ablation: inapplicable. The diff adds no guard, no schema and no executable line — it is prose in a governed document. There is nothing to mutate and no test whose failure could be demonstrated, so none was invented.

Escalation antecedent — re-checked cheaply, unchanged

Triage set: 「若发现已有实现按现文字写成,立即抬到 p1 并回帖」. Re-checked on bc1c1ce494 rather than re-derived: runFilesToReferencesMigration is still backfill → verify → record-flag, and unquote, JSON_UNQUOTE, the Postgres text-extract operator, json_extract and ALTER COLUMN all return 0 hits in packages/services/service-storage/src/files-to-references-migration.ts and packages/cli/src/commands/migrate/files-to-references.ts. ⇒ step 3 is still unbuilt, nothing implements the old wording, p2 holds.

维护者速读(草稿)

改了什么

只改了一份治理文档 docs/adr/0104-field-runtime-value-shape-contract.md 里的一段执行说明。原文承诺迁移第 3 步会「遇到第一个非 JSON 字符串的单元格就中止」,紧接着开的 Postgres 方子却做不到这件事。现在把中止拆成 3a(动任何 DDL 之前先跑的预检)和 3b(真正的搬列动作),并把两次实测结果写进置信缺口。⛔ 需求原话一字未改,决定也一个字没动。

为什么改

这段文字挡着 step 3 的实现。照现在的写法去实现,那条 ALTER TABLE … USING 语句会静默压平恰恰是回填没能转换的那些行 —— 也就是对账存在的意义所在。今天还没有人实现 step 3(本轮已复核),所以这是修正它成本最低的时刻:改文字,不改数据,不改代码。

风险与代价(含回滚)

风险极低:单文件、纯文字、零代码、零发布物。回滚 = revert 这一个提交,没有任何下游产物需要跟着退。真正的代价在不改这一侧:留着它,下一个实现者会写出那条破坏性语句。

席位意见

(留空,待席位定稿)

你要做的

这是治理面 ⇒ 本 PR 恒为 draft,由维护者人工合并。请确认三件事:① 需求原话确实一字未改;② MySQL 与「别的 Postgres 写法能否自行中止」两处仍标为未测,没有被顺手补全;③ 3a/3b 的拆法读起来就是你要的执行顺序。确认后人工直合。


Generated by Claude Code

…retype

ADR-0104's 2026-09-05 addendum promised that step 3 of
`os migrate files-to-references --apply` moves a media column while "aborting
on the first cell that is not a JSON string", and prescribed a Postgres
`USING` clause in the paragraph below it. Rehearsed on a live PostgreSQL
16.13, those two sentences contradict each other: the retype does not abort,
it converts, and an un-backfilled inline object is flattened to literal text
in the varchar column.

The requirement is correct and is unchanged. What was optimistic is the
statement list under it, so the step now reads as 3a (a pre-check over every
cell, before any DDL or rewrite, carrying the requirement verbatim) and 3b
(the move, only if 3a found none). The sketch paragraph records what both
rehearsed dialects actually did — Postgres flattens, SQLite silently skips —
and names the measured discriminator for each.

Confidence gap 1 gains its one Postgres exception: that single statement moves
from reasoned to measured. Gap 2 is partly closed by a rehearsal whose result
was negative. MySQL stays entirely unmeasured, and whether some other Postgres
`USING` expression could abort on its own stays an open question rather than a
finding — both are carried forward under gap 2 as open.

No decision is re-opened: D1/D3, the window and the end-state column are
untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg

os-musk commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator Author

席位意见 —— domain:engine 执行 PM 席(补 维护者速读 里留空的那一格)

⛔ 这条写成评论而不是改正文:AGENTS.md 记着 PR 正文的归属页脚在通道动作上都不一致,并明令 ⛔ 不要重发一份已经带页脚的正文。这份正文创建后已读回、与发出的字节一致、session-URL 页脚完好 —— 那个状态比把意见塞进正文块更值钱。

本席的意见:建议直合。

三条理由,都是可核的:

  1. 它改的是一段挡着实现的执行说明,而不是一个决定。 需求原话 "aborting on the first cell that is not a JSON string" 一字未改(空白归一后前后各恰好一次);D1/D3、窗口、终态列全部未动。⇒ 这份 PR 不要求维护者重新裁决任何事。
  2. 它带来的新事实,是两次真跑出来的,而且推翻了一个此前没人写下的对称假设。 两种方言的失败方向相反:Postgres 的 USING (col #>> '{}') 被接受并把未回填的对象压平成字面文本;SQLite 的 json_type='text' 闸门则把同一个单元格静默跳过、原样留在旧编码里。两者都不是那个被承诺的中止。 这正是把中止拆成 3a(任何 DDL 之前的预检)的依据,而这个依据在本轮之前不存在。
  3. 不改的代价比改大得多。 今天没有人实现 step 3(本轮已复核:runFilesToReferencesMigration 仍是 backfill → verify → record-flag,五个关键词在两个文件里 0 命中)。⇒ 现在是修这段文字最便宜的时刻:改字,不改数据,不改代码,零发布物。留着它,下一个实现者会照着写出那条静默销毁恰恰是回填没能转换的那些行的语句 —— 而那些行正是对账存在的意义。

⚠️ 本席请维护者重点看的,就是速读里那三条,原样保留

① 需求原话确实一字未改;② MySQL 与「别的 Postgres 写法能否自行中止」两处仍标为未测,没有被顺手补全 —— 这是本卡最容易出错的地方,而这份 PR 把它们写成了仍然开放的问题而不是发现;③ 3a/3b 的拆法读起来是否就是你要的执行顺序。

本席核过、可直接采信的部分

  • 仪器点名了:PostgreSQL 16.13 (Ubuntu 16.13-0ubuntu0.24.04.1)sqlite_version() = 3.51.2,版本号是从服务器读回的,不是假设的。⭐ 且整段 Postgres 脚本跑在 psql -v ON_ERROR_STOP=1 之下 —— 那次运行自己的 exit 0 就是「没有任何语句中止」的证明,比读输出下结论强得多。
  • changeset 路线是量出来的,不是「文档路径当然不发布」:git ls-files 枚举 70 个非私有具名 package.json,没有一个docs/adr 的祖先目录,没有一个files[]docs.. ⇒ 没有任何发布物够得着这条路径。
  • 17 个门禁族全部 exit 0,0 NOT-MEASURED;其中 check:doc-formula-expressions 首次返回 exit 3 PREREQUISITE NOT MET —— 那是未测量而不是通过 —— 先补建它点名的闭包再重跑到真实判决。
  • 消融报告为 INAPPLICABLE 而不是编一个:治理文档里的散文没有可变异的守卫。

⛔ 席位不做的事

⛔ 本 PR 恒为 draft,本席不标 ready、不入队、不开自动合并,也不合并 —— 治理面由维护者人工直合。⛔ 本席也未编辑正文(理由见开头)。#16183 在人工合并之前保持 pm:dispatched


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

维护者速读(终稿)—— PR #16972 · 卡 #16183 · 治理面 docs/adr/0104

改了什么 —— 只改一份 ADR 文档的一段执行说明。原文承诺迁移第 3 步「遇到第一个非 JSON 字符串的单元格就中止」,紧接着开出的 Postgres USING 方子做不到。现在把第 3 步拆成 3a(动任何 DDL 之前跑的预检,中止条件原话不变)和 3b(真正搬列,只在 3a 无发现时执行),并把两次实测(PostgreSQL 16.13、SQLite 3.51.2)写进置信缺口 1、2。决定、窗口、终态列一字未动。

为什么改 —— 照原文实现,ALTER TABLE … USING (col #>> '{}')静默压平恰恰是回填没能转换的那些行 —— 对账存在的意义所在。第 3 步今天还没有人实现(PR 正文复核:unquote / JSON_UNQUOTE / json_extract / ALTER COLUMN 在两处实现文件里 0 命中),所以现在改文字最便宜。

风险与代价(含回滚) —— 单文件、纯散文、零代码、零发布物(70 个可发布包的 files[] 没有一个够到 docs/)。回滚 = revert 一个提交。不改的代价:下一个实现者会写出那条破坏性语句。

席位意见 —— 本席按 diff 逐段核对了草稿:① 需求原话「aborting on the first cell that is not a JSON string」在 3a 里逐字保留;② MySQL 与「其它 Postgres USING 写法能否自行中止」两处明写为未测,没有被顺手补全;③ 3a/3b 的拆法与第 4、5 步的顺序自洽。34 项 check 按名聚合全绿(21 success / 9 skipped / 0 fail),mergeable_state: clean。建议合并。

你要做的 —— 治理面 ⇒ 本 PR 恒为 draft,请你人工直合(review 已请到 os-zhuang / hotlong)。⛔ 本席不翻 ready、不入队、不挂 auto-merge。


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review September 9, 2026 03:33
@os-zhuang
os-zhuang enabled auto-merge September 9, 2026 03:33
@os-zhuang
os-zhuang added this pull request to the merge queue Sep 9, 2026
Merged via the queue into main with commit 881ce2b Sep 9, 2026
40 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-16183-adr-0104-step3-abort-precheck branch September 9, 2026 04:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation needs-user-decision size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

docs(adr-0104): step 3 promises an abort the prescribed Postgres USING clause does not perform — measured, it flattens the cell instead

4 participants